Council of Claudes: orchestrator agent for cross-persona comment dedup#229
Conversation
Inserts an "intelligence layer" between persona fan-out and posting that removes redundant inline comments (the #1 reviewer complaint). The orchestrator does ONE thing — dedup; all other quality control stays with the personas (Lever 2, later). - scripts/orchestrator.md: new kagent agent systemMessage. Input = all inline comments as {id,persona,file,line,body}; output = {clusters:[{survivor, duplicates,reason}]} (decisions only, never rewrites bodies); conservative ("when in doubt, don't group"); clarity-first survivor; comments-only (no diff). - council_of_claudes.js: collect all personas' anchorable inline findings with stable ids, call the orchestrator (async submit+poll, reusing the agent helper), then drop only flagged duplicates. Defensive + lossless: omission=keep, keep-wins on conflict, unknown ids ignored; ANY failure / unset secret → post everything unfiltered. Summaries untouched (v1). - council_of_claudes.yml: map ORCHESTRATOR_AGENT_URL/_TOKEN in env. Gated on its secret → no-op until the agent is created. Design + decisions in orchestrator-design.md. Validated with mock tests (dedup, graceful degradation, keep-wins, unknown-id handling). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔎 Council of Claudes — Correctness
Note
Correctness lens · bugs · completeness · concurrency · edge cases
No correctness issues found
The orchestrator integration appears to meet the stated design goals: it deduplicates only inline comments, is gated on its own secrets, degrades safely to “post everything” on any failure/unset secret, and applies defensive parsing and application of decisions (unknown IDs ignored, keep-wins on conflicts). The ephemeral per-run IDs are unique per persona and used consistently through apply/post. Logging provides useful visibility without affecting behavior. I don’t see any obvious logic bugs, missed edge cases, or API misuse in the added paths based on the diff provided.
No line-specific findings.
🤖 Council of Claudes · 0 inline comment(s)
There was a problem hiding this comment.
🧪 Council of Claudes — Maintainability & Tests
Tip
Maintainability & Tests lens · simplicity · tests · docs · idioms
Well-scoped, defensively implemented addition; however, the new dedup logic lands without automated tests. Recommend extracting helper logic for unit tests and adding a small self-test workflow; a couple of minor maintainability/documentation tweaks suggested.
- Tests: This PR introduces nontrivial, stateful logic (parseClusters, applyClusters, orchestrateDedup) at the core of comment posting, but there are no automated tests. Given this sits on the critical path of review output, a lightweight Node-based unit test suite would pay back quickly and prevent regressions (e.g., shape drift in the orchestrator’s response, parsing fallbacks, keep-wins behavior, unknown-id handling).
- Separation for testability: Consider moving the orchestrator helpers into a small module (or at least exporting them via a test-only handle) to enable hermetic unit tests without invoking GitHub APIs or kagent.
- Documentation cross-link/versioning: You’ve added an excellent design doc and an orchestrator system prompt. To prevent future drift, add an explicit “schema v1” tag and a “keep in sync with council_of_claudes.js” pointer in orchestrator.md, and a reciprocal link in the JS.
- Optional CI: A tiny GH Action job that runs “node” unit tests for this script would give fast feedback without touching Calico’s main CI.
Line-specific findings:
🤖 Council of Claudes · 9 inline comment(s)
There was a problem hiding this comment.
🛡️ Council of Claudes — Security
Caution
Security lens · validation · secrets · authz · isolation
2 potential security issues
- Outbound call to a third-party “orchestrator” service is gated on secrets (good), but the URL is taken verbatim from env without scheme/host validation. A misconfigured secret could send the dedup payload and bearer token to a plaintext HTTP endpoint or an unintended host, risking token leakage and exfiltration of PR-derived data. Recommend enforcing HTTPS and a host allowlist before making the request.
- Double-check workflow trigger/context so these new secrets are never exposed in untrusted PR contexts (e.g., forks). The code itself doesn’t echo tokens, but if the job runs with secrets on forked PRs, an attacker could cause external calls or glean endpoint details via error logs. Ensure this job only runs where GitHub will not expose repository secrets to forks, or add explicit guards.
🤖 Council of Claudes · 3 inline comment(s)
There was a problem hiding this comment.
Council of Claudes — Nell
Important
Nell lens · simulated reviewer · simplicity · naming · error handling · keep useful comments
No correctness issues found; looks thoughtfully defensive. A couple of small clarity/robustness nits and one comment/code mismatch. If we can, I'd love a tiny unit harness for parseClusters/applyClusters (even as a hack/ script) to lock in the keep-wins/unknown-id behaviours — but fine to defer.
- The “pure decision logic” comment on applyClusters reads a bit stronger than what the function does (it logs). Either move the logs out or soften the comment?
- Input robustness: we already parse fenced JSON from the orchestrator, but we don’t fence the input JSON we send to it. Wrapping the payload in a ```json block might help its parser.
- parseClusters only tries the first fenced block; consider scanning all fenced blocks (minor).
- nit: safe(cl.reason) — safe handles undefined/null, no? Just checking.
Line-specific findings:
🤖 Council of Claudes · 4 inline comment(s)
There was a problem hiding this comment.
Council of Claudes — Casey
Warning
Casey lens · simulated reviewer · testing discipline · API design · simplicity · robustness
No correctness issues found. I think the overall shape looks good — conservative, “lossless by construction,” and well-fenced behind secrets so this is a no-op until configured. Nice touches on the keep-wins rule, unknown-id defense, and logging enough to audit the orchestrator’s decisions. My main nits are around simplifying/strengthening the JSON parsing path and making the request text a bit more prescriptive so we don’t need such a permissive parser. One thought on tests: since parseClusters/applyClusters are pure and central to the behavior, I think a tiny unit-test harness for those two (even as a simple node script in hack/ that runs in CI) would pull its weight — we generally try to land tests alongside behavior changes. WDYT?
- I am a little bit skeptical of the “brace slice” fallback in parseClusters — it feels clever but hard to reason about under weird outputs. If we can make the orchestrator reliably respond with only JSON (or a fenced block), I think we can drop that path and reduce the chance of accidentally parsing the wrong substring.
- Minor robustness: safe(e.message) assumes an Error-like object; worth guarding for thrown strings/unknowns.
- Small ergonomics: adding one sentence to the orchestrator prompt message (“Respond with only the JSON object, no prose”) would let us tighten the parser and rely on the fence path — less parsing surface area, fewer surprises.
- Non-blocking: IDs are ephemeral by design, which is fine — if you ever want easier cross-referencing in logs, including file:line in the id or in the keep/drop logs might help debugging. Not required though.
🤖 Council of Claudes · 6 inline comment(s)
There was a problem hiding this comment.
Pull request overview
Adds an “orchestrator” stage to the Council of Claudes GitHub Action that deduplicates redundant inline review comments across personas, while preserving original comment bodies/persona attribution and failing open (posting everything) when unconfigured or on any error.
Changes:
- Introduces an orchestrator system prompt (
orchestrator.md) that instructs the agent to return duplicate clusters by stable comment IDs only. - Extends
council_of_claudes.jsto collect all anchorable inline findings across personas, call the orchestrator, and skip posting IDs marked as duplicates (keep-wins; unknown IDs ignored; failure → drop nothing). - Wires optional orchestrator URL/token secrets into the workflow environment.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
.github/workflows/scripts/orchestrator.md |
Defines the orchestrator agent contract (input schema, dedup criteria, and strict JSON output shape). |
.github/workflows/scripts/council_of_claudes.js |
Implements cross-persona inline-comment dedup orchestration with defensive parsing and lossless fallback behavior. |
.github/workflows/council_of_claudes.yml |
Adds optional ORCHESTRATOR_AGENT_URL/TOKEN env wiring; workflow remains a no-op when unset. |
- orchestrator.md: model ref → claude-opus-4-8 (the cluster now offers more models). Persona docs stay gpt-5: personas are held on gpt-5 for this evaluation so the orchestrator's effect is isolated vs the gpt-5-generated labeled baselines (persona model upgrade is a separate future iteration). - gen-benchmark-pr.sh: optional ITER=<label> → coc-sample-<N>-<label>-* branches + a [label] PR title tag, so fresh duplicate PRs coexist with earlier labeled baselines instead of updating them in place. Empty by default (unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the Council's review of #229: 1. orchestrateDedup wrapped in try/catch — makes the "post everything on failure" guarantee robust even if a callee starts throwing in future. 2. Extracted applyClusters() — pure decision logic (keep-wins/unknown-id), separated from env/network I/O for readability + testability. 3. parseClusters fence regex now also matches single-line ```json fences. 4. core.warning when an id is listed as both survivor and duplicate (keep-wins still saves it) — surfaces a misbehaving orchestrator. 5. Clarified that finding ids are run-scoped/ephemeral (comment + softened "stable id" wording in orchestrator.md). Declined / deferred: HTTPS-enforcement + third-party data-egress (applies to all agent calls; already in the hardening backlog), msgId RUN_ATTEMPT (marginal). Also: commit the orchestrator design doc to hack/council-of-claudes/ (resolves the dangling PR-body reference; linked from the README). Re-validated with mock tests (dedup, single-line fence, graceful degradation, keep-wins, unknown-id, orchestrator-unreachable) — all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| return null; | ||
| } | ||
|
|
||
| // --- Orchestrator: cross-persona deduplication of inline comments --- |
There was a problem hiding this comment.
🧪 Maintainability & Tests — Consider extracting the “Orchestrator” block (parseClusters, applyClusters, orchestrateDedup and the ORCH_* constants) into a dedicated module (e.g., .github/workflows/scripts/orchestrator.js) and import it here. This reduces file size/complexity and makes the logic easier to unit test in isolation. If you prefer not to split files, minimally export the helpers for tests:
// near the bottom, before module.exports default:
module.exports._orchestratorTest = { parseClusters, applyClusters, orchestrateDedup };This keeps production behavior unchanged while enabling direct imports in a small Node test.
| // Robustly extract the orchestrator's { clusters: [...] } JSON from its response | ||
| // (tolerates a ```json fence — single- or multi-line — or stray prose). Returns | ||
| // the parsed object or null. | ||
| function parseClusters(text) { |
There was a problem hiding this comment.
🧪 Maintainability & Tests — parseClusters has several tolerant fallback paths (fenced json, substring braces, raw text). Add unit tests covering:
- Pure JSON object, fenced single-line and multi-line ```json blocks, and responses with leading/trailing prose.
- Multiple fences present (ensure the right candidate is chosen).
- Invalid JSON and non-matching shapes (returns null).
A minimal node:test or assert-based test file under .github/workflows/scripts/ can exercise these without any build tooling.
| // testable. Defensive against arbitrary LLM output: honor a cluster only if its | ||
| // survivor is a known id; drop only known duplicate ids; keep-wins (an id that | ||
| // survives anywhere is never dropped); anything not mentioned is implicitly kept. | ||
| function applyClusters({ core, allInline, parsed }) { |
There was a problem hiding this comment.
🧪 Maintainability & Tests — applyClusters is pure and central to safety (omission=keep, keep-wins, unknown-id ignore). Add unit tests for:
- Unknown ids in survivor/duplicates are ignored (drop set empty).
- Conflicting listing (an id appears as survivor in one cluster and duplicate in another) → keep-wins triggers; drop set excludes that id and a warning is logged.
- Survivor with empty duplicates is ignored (no-op).
- Multiple clusters applied with disjoint ids.
Codify these to prevent future regressions in the safety guarantees.
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + | ||
| `Identify redundant duplicate groups and return the clusters JSON per your instructions.\n\n${payload}`; | ||
|
|
||
| const out = await reviewWithAgent({ core, title: 'Orchestrator', url, token, messageText, msgId }); |
There was a problem hiding this comment.
🧪 Maintainability & Tests — Please verify reviewWithAgent already has a bounded poll/timeout. If not, consider adding an explicit timeout via AbortController/fetch timeout or a max poll duration passed into reviewWithAgent, so a stuck orchestrator can’t stall posting indefinitely. Add a brief comment here pointing to the timeout semantics to aid future maintainers.
| } | ||
| findings.forEach((f, i) => { | ||
| if (anchors.get(f.file)?.has(f.line)) { | ||
| // Run-scoped ephemeral id: generated and consumed within this single |
There was a problem hiding this comment.
🧪 Maintainability & Tests — Great inline rationale for ephemeral ids. For maintainability, add a brief note that these ids are intentionally not correlated with GitHub review comment IDs and exist only within a single run to drive orchestrator decisions. This helps future refactors avoid accidentally persisting or reusing them across runs.
| // any failure / unconfigured (returns empty → post everything). | ||
| const drop = await orchestrateDedup({ core, allInline }); | ||
|
|
||
| // 4c. Post per persona: delete prior no-reply inline, post surviving inline |
There was a problem hiding this comment.
🧪 Maintainability & Tests — Now that dedup happens globally, consider logging a short per-persona summary in addition to the global dropped count (e.g., “Correctness: posted X, dropped Y”), to make it easier to spot if one persona is consistently being deduped away. This is optional, but it can help tune prompt-level levers later.
| } | ||
| } | ||
| core.info(`Done: ${summaries}/${active.length} summaries, ${inlineTotal} inline comment(s) on PR #${pull_number}`); | ||
| core.info(`Done: ${summaries}/${active.length} summaries, ${inlineTotal} inline posted, ${dropped} duplicate(s) dropped on PR #${pull_number}`); |
There was a problem hiding this comment.
🧪 Maintainability & Tests — Nice addition to the final summary. If you add unit tests, also assert the log string format in at least one test to detect accidental changes (people often grep CI logs for this).
| @@ -0,0 +1,67 @@ | |||
| # Review Orchestrator — Council of Claudes | |||
|
|
|||
| Paste this as the `systemMessage` when creating the **orchestrator** agent in the | |||
There was a problem hiding this comment.
🧪 Maintainability & Tests — Add a simple version tag and cross-link to the JS that consumes this schema to prevent drift:
- At the top: “Schema: v1 (keep in sync with .github/workflows/scripts/council_of_claudes.js parseClusters/applyClusters)”
- If the output shape changes, the code must be updated alongside this file in the same PR.
|
|
||
| baseBr="coc-sample-$N-base" | ||
| headBr="coc-sample-$N-head" | ||
| # Optional iteration label (ITER=v2) → parallel duplicate PRs for the same |
There was a problem hiding this comment.
🧪 Maintainability & Tests — The ITER suffix is a helpful addition. Consider adding a one-liner to hack/council-of-claudes/README.md under the Tools section documenting usage (e.g., “Use ITER=v2 to create parallel duplicate PRs for the same sample: ITER=v2 ./gen-benchmark-pr.sh ”). This makes the knob discoverable without having to read the design doc.
| NELLJERRAM_AGENT_TOKEN: ${{ secrets.NELLJERRAM_AGENT_TOKEN }} | ||
| CASEYDAVENPORT_AGENT_URL: ${{ secrets.CASEYDAVENPORT_AGENT_URL }} | ||
| CASEYDAVENPORT_AGENT_TOKEN: ${{ secrets.CASEYDAVENPORT_AGENT_TOKEN }} | ||
| # Orchestrator (cross-persona dedup); if unset, all comments are posted unfiltered. |
There was a problem hiding this comment.
🛡️ Security — The workflow introduces new secrets (ORCHESTRATOR_AGENT_URL/TOKEN). If this job can run in contexts where repository secrets are available to untrusted fork PRs, an attacker could cause outbound calls to an attacker-controlled URL or probe for environment details through log messages. Mitigation: ensure the workflow only runs in trusted contexts (e.g., not on untrusted forks) or add an explicit guard like if: ${{ !github.event.pull_request.head.repo.fork }}. Alternatively, trigger via pull_request_target with extreme caution and strict input validation on all consumed data.
| // "post everything on failure" guarantee robust even if a callee starts throwing. | ||
| async function orchestrateDedup({ core, allInline }) { | ||
| const empty = new Set(); | ||
| const url = process.env[ORCH_URL_ENV]; |
There was a problem hiding this comment.
🛡️ Security — The orchestrator URL is read from the environment and used for an outbound request without validating the scheme or host. A misconfigured URL could downgrade to plaintext HTTP or redirect to an unintended host, leaking the dedup payload and the bearer token. Mitigation: before calling reviewWithAgent, validate the URL:
- Require HTTPS:
new URL(url).protocol === 'https:' - Optionally enforce an allowlist for hostnames (e.g.,
agents.tigera.ai) - Reject/skip orchestration (post everything) if validation fails, and log a warning without printing the raw URL.
Example:
const u = new URL(url);
if (u.protocol !== 'https:' || !['agents.tigera.ai'].includes(u.hostname)) {
core.warning('Orchestrator URL failed validation — skipping dedup and posting all comments.');
return empty;
}| if (!parsed) { core.warning('Orchestrator: unparseable response — posting all comments unfiltered'); return empty; } | ||
| return applyClusters({ core, allInline, parsed }); | ||
| } catch (e) { | ||
| core.warning(`Orchestrator: unexpected error (${safe(e.message)}) — posting all comments unfiltered`); |
There was a problem hiding this comment.
🛡️ Security — On error, the code logs safe(e.message). Depending on the underlying HTTP client, error messages can include request URLs or other connection details. While tokens aren’t in the URL here, exposing internal hostnames in public logs may still be undesirable. Mitigation: redact/omit URLs/hostnames in error messages (e.g., replace them with a constant tag) or map exceptions to a generic message. Ensure reviewWithAgent also avoids logging tokens/URLs on failures.
| return null; | ||
| } | ||
|
|
||
| // Pure decision logic: given the inline comments and the orchestrator's parsed |
There was a problem hiding this comment.
Nell — The comment says “Pure decision logic … No env / network — directly unit testable.” but the function logs via core.info/core.warning. So it isn’t pure anymore. Soften the comment, or move the logging into the caller and keep this one side‑effect free?
| // Pure decision logic: given the inline comments and the orchestrator's parsed | |
| // Decision logic: given the inline comments and the orchestrator's parsed | |
| // clusters, return the SET of ids to drop. Defensive against arbitrary LLM | |
| // output: honor a cluster only if its survivor is a known id; drop only known | |
| // duplicate ids; keep-wins (an id that survives anywhere is never dropped); | |
| // anything not mentioned is implicitly kept. |
| function parseClusters(text) { | ||
| if (!text) return null; | ||
| const tries = [text.trim()]; | ||
| const fence = text.match(/```[a-zA-Z]*\s*([\s\S]*?)```/); // handles single-line fences too |
| survivors.add(cl.survivor); | ||
| const dups = Array.isArray(cl.duplicates) ? cl.duplicates.filter(d => known.has(d)) : []; | ||
| dups.forEach(d => candidates.add(d)); | ||
| if (dups.length) core.info(` orchestrator: keep ${cl.survivor}, drop [${dups.join(', ')}] — ${safe(cl.reason)}`); |
| const payload = JSON.stringify(allInline.map(c => | ||
| ({ id: c.id, persona: c.persona, file: c.file, line: c.line, body: c.body }))); | ||
| const messageText = | ||
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + |
There was a problem hiding this comment.
Nell — We send the raw JSON array inline in messageText. Do we want to fence it as JSON to help the orchestrator’s parser? Something like:
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + | |
| const messageText = | |
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + | |
| `Identify redundant duplicate groups and return the clusters JSON per your instructions.\n\n` + | |
| "```json\n" + payload + "\n```"; |
| const tries = [text.trim()]; | ||
| const fence = text.match(/```[a-zA-Z]*\s*([\s\S]*?)```/); // handles single-line fences too | ||
| if (fence) tries.push(fence[1].trim()); | ||
| const first = text.indexOf('{'), last = text.lastIndexOf('}'); |
There was a problem hiding this comment.
Casey — I think this lenient “first { … last }” slice might be too permissive — it could grab the wrong substring if prose contains braces or multiple JSON-ish snippets. Since you already (a) try the whole message and (b) prefer a fenced block, can we drop this fallback (or at least only use it when there’s exactly one well-formed object between balanced backticks)?
| const first = text.indexOf('{'), last = text.lastIndexOf('}'); | |
| const fence = text.match(/```[a-zA-Z]*\s*([\s\S]*?)```/); | |
| if (fence) tries.push(fence[1].trim()); | |
| // Drop the brace-slice fallback to avoid accidental mis-parses; rely on fenced or plain-JSON only. |
Reason: simpler and less error-prone; the agent instructions can enforce JSON-only or a fenced block.
| const payload = JSON.stringify(allInline.map(c => | ||
| ({ id: c.id, persona: c.persona, file: c.file, line: c.line, body: c.body }))); | ||
| const messageText = | ||
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + |
There was a problem hiding this comment.
Casey — I wonder if we should be more prescriptive here to reduce parser flexibility later — can we explicitly ask for “only the JSON object, no prose” in the message? That’d let us rely on the fence/plain-JSON path and avoid the broad brace-slice.
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + | |
| const messageText = | |
| `Here are ${allInline.length} inline review comments from one pull request, as a JSON array. ` + | |
| `Identify redundant duplicate groups and return ONLY the JSON object described (no prose). ` + | |
| `A fenced \`\`\`json block is acceptable.\n\n${payload}`; |
| if (!parsed) { core.warning('Orchestrator: unparseable response — posting all comments unfiltered'); return empty; } | ||
| return applyClusters({ core, allInline, parsed }); | ||
| } catch (e) { | ||
| core.warning(`Orchestrator: unexpected error (${safe(e.message)}) — posting all comments unfiltered`); |
There was a problem hiding this comment.
Casey — Do we know if reviewWithAgent ever throws a non-Error (e.g., a string)? safe(e.message) will be undefined in that case — might be safer to stringify the whole error.
| core.warning(`Orchestrator: unexpected error (${safe(e.message)}) — posting all comments unfiltered`); | |
| core.warning(`Orchestrator: unexpected error (${safe(e && (e.message || String(e)))}) — posting all comments unfiltered`); |
No big deal though if safe() already handles undefined cleanly.
| // `i` is the index into `findings` (gaps where entries were folded are | ||
| // harmless — ids only need per-persona uniqueness); never persisted, so | ||
| // it must not be correlated across runs or against existingComments. | ||
| const id = `${p.key}-${i}`; |
There was a problem hiding this comment.
Casey — I think using i is fine as an ephemeral id, but would it help debuggability to include an easy-to-eye-catch hint (e.g., file:line) in the id or (maybe better) in the “keep/drop” logs? The id never persists, so this is just about making the action logs easier to correlate. Fine to leave as-is, though.
| const id = `${p.key}-${i}`; | |
| const id = `${p.key}-${i}`; // consider `${p.key}-${f.file}:${f.line}-${i}` for easier log correlation |
| survivors.add(cl.survivor); | ||
| const dups = Array.isArray(cl.duplicates) ? cl.duplicates.filter(d => known.has(d)) : []; | ||
| dups.forEach(d => candidates.add(d)); | ||
| if (dups.length) core.info(` orchestrator: keep ${cl.survivor}, drop [${dups.join(', ')}] — ${safe(cl.reason)}`); |
There was a problem hiding this comment.
Casey — nit: This log can get a bit long if reasons are verbose. Maybe cap reason length to keep the action log tidy?
| if (dups.length) core.info(` orchestrator: keep ${cl.survivor}, drop [${dups.join(', ')}] — ${safe(cl.reason)}`); | |
| const reason = safe(cl.reason || ''); | |
| const trimmed = reason.length > 200 ? `${reason.slice(0, 200)}…` : reason; | |
| if (dups.length) core.info(` orchestrator: keep ${cl.survivor}, drop [${dups.join(', ')}] — ${trimmed}`); |
Totally optional; up to you.
| # Optional iteration label (ITER=v2) → parallel duplicate PRs for the same | ||
| # original, so a new run doesn't clobber an earlier labeled baseline. Empty by | ||
| # default (branches stay coc-sample-<N>-base/head). | ||
| SUFFIX="${ITER:+-$ITER}" |
There was a problem hiding this comment.
Casey — Small portability question: this uses bash parameter expansion (:+). I don’t remember the shebang offhand — is this file bash-scripted already? If not, we should add/update it to bash to avoid sh quirks. WDYT?
| SUFFIX="${ITER:+-$ITER}" | |
| # At top of file: | |
| #!/usr/bin/env bash |
If it’s already there, ignore me.
What
Adds the orchestrator — an intelligence layer between persona fan-out and posting that removes redundant inline comments (multiple personas making the same point), the #1 reviewer complaint from team feedback.
Per the design (
orchestrator-design.md), it does one thing — dedup. All other quality control (nit/volume reduction) stays with the individual personas as a separate future iteration. This keeps the orchestrator simple and avoids a single point of complexity, consistent with the multi-agent philosophy.How it works
{id, persona, file, line, body}(comments-only — no diff).{clusters:[{survivor, duplicates, reason}]}— decisions only, never rewrites bodies.Safety (lossless by construction)
To activate
scripts/orchestrator.md(modelgpt-5).ORCHESTRATOR_AGENT_URL/ORCHESTRATOR_AGENT_TOKENsecrets.Validation
Mock-tested: dedup (incl. fenced-JSON parsing), graceful degradation (unconfigured + garbage response), keep-wins, and unknown-id handling — all pass. End-to-end validation will be a fresh duplicate PR vs. the labeled baselines (#218/#200/#201), checking that the redundant clusters humans flagged collapse.
🤖 Generated with Claude Code